Детальний огляд ключового слова 'infer' у TypeScript, його використання в умовних типах для потужних маніпуляцій та покращення чіткості коду.
Умовний вивід типів: Оволодіння ключовим словом 'infer' у TypeScript
Система типів TypeScript пропонує потужні інструменти для створення надійного та легкопідтримуваного коду. Серед цих інструментів умовні типи виділяються як універсальний механізм для вираження складних взаємозв'язків типів. Ключове слово infer, зокрема, відкриває розширені можливості в рамках умовних типів, дозволяючи виконувати складне вилучення та маніпулювання типами. Цей вичерпний посібник дослідить тонкощі infer, надаючи практичні приклади та ідеї, щоб допомогти вам освоїти його використання.
Розуміння умовних типів
Перш ніж зануритися в infer, важливо зрозуміти основи умовних типів. Умовні типи дозволяють визначати типи, що залежать від умови, подібно до тернарного оператора в JavaScript. Синтаксис має такий вигляд:
T extends U ? X : Y
Тут, якщо тип T може бути присвоєний типу U, результуючим типом є X; в іншому випадку — Y.
Приклад:
type IsString<T> = T extends string ? true : false;
type StringCheck = IsString<string>; // type StringCheck = true
type NumberCheck = IsString<number>; // type NumberCheck = false
Цей простий приклад демонструє, як умовні типи можуть бути використані для визначення того, чи є тип рядком. Ця концепція поширюється на складніші сценарії, відкриваючи шлях для ключового слова infer.
Представляємо ключове слово 'infer'
Ключове слово infer використовується в гілці true умовного типу для введення змінної типу, яку можна вивести з типу, що перевіряється. Це дозволяє витягувати певні частини типу та використовувати їх у результуючому типі.
Синтаксис:
T extends (infer R) ? X : Y
У цьому синтаксисі R є змінною типу, яка буде виведена зі структури T. Якщо T відповідає шаблону, R буде містити виведений тип, а результуючим типом буде X; в іншому випадку — Y.
Основні приклади використання 'infer'
1. Виведення типу повернення функції
Поширеним випадком використання є виведення типу повернення функції. Це можна досягти за допомогою такого умовного типу:
type ReturnType<T extends (...args: any) => any> = T extends (...args: any) => infer R ? R : any;
Пояснення:
T extends (...args: any) => any: Це обмеження гарантує, щоTє функцією.(...args: any) => infer R: Цей шаблон відповідає функції та виводить тип повернення якR.R : any: ЯкщоTне є функцією, результуючим типом єany.
Приклад:
function greet(name: string): string {
return `Hello, ${name}!`;
}
type GreetingReturnType = ReturnType<typeof greet>; // type GreetingReturnType = string
function calculate(a: number, b: number): number {
return a + b;
}
type CalculateReturnType = ReturnType<typeof calculate>; // type CalculateReturnType = number
Цей приклад демонструє, як ReturnType успішно витягує типи повернення функцій greet та calculate.
2. Виведення типу елемента масиву
Ще одним частим випадком використання є вилучення типу елемента масиву:
type ElementType<T> = T extends (infer U)[] ? U : never;
Пояснення:
T extends (infer U)[]: Цей шаблон відповідає масиву та виводить тип елемента якU.U : never: ЯкщоTне є масивом, результуючим типом єnever.
Приклад:
type StringArrayElement = ElementType<string[]>; // type StringArrayElement = string
type NumberArrayElement = ElementType<number[]>; // type NumberArrayElement = number
type MixedArrayElement = ElementType<(string | number)[]>; // type MixedArrayElement = string | number
type NotAnArray = ElementType<number>; // type NotAnArray = never
Це показує, як ElementType правильно виводить тип елемента різних типів масивів.
Розширене використання 'infer'
1. Виведення параметрів функції
Подібно до виведення типу повернення, ви можете вивести параметри функції, використовуючи infer та кортежі:
type Parameters<T extends (...args: any) => any> = T extends (...args: infer P) => any ? P : never;
Пояснення:
T extends (...args: any) => any: Це обмеження гарантує, щоTє функцією.(...args: infer P) => any: Цей шаблон відповідає функції та виводить типи параметрів як кортежP.P : never: ЯкщоTне є функцією, результуючим типом єnever.
Приклад:
function logMessage(message: string, level: 'info' | 'warn' | 'error'): void {
console.log(`[${level.toUpperCase()}] ${message}`);
}
type LogMessageParams = Parameters<typeof logMessage>; // type LogMessageParams = [message: string, level: "info" | "warn" | "error"]
function processData(data: any[], callback: (item: any) => void): void {
data.forEach(callback);
}
type ProcessDataParams = Parameters<typeof processData>; // type ProcessDataParams = [data: any[], callback: (item: any) => void]
Parameters витягує типи параметрів як кортеж, зберігаючи порядок та типи аргументів функції.
2. Вилучення властивостей з об'єктного типу
infer також можна використовувати для вилучення певних властивостей з об'єктного типу. Це вимагає складнішого умовного типу, але дозволяє потужні маніпуляції з типами.
type PickByType<T, U> = {
[K in keyof T as T[K] extends U ? K : never]: T[K];
};
Пояснення:
K in keyof T: Це ітерує всі ключі типуT.T[K] extends U ? K : never: Цей умовний тип перевіряє, чи може тип властивості за ключемK(тобто,T[K]) бути присвоєний типуU. Якщо так, ключKвключається до результуючого типу; в іншому випадку він виключається за допомогоюnever.- Вся конструкція створює новий об'єктний тип лише з тими властивостями, чиї типи розширюють
U.
Приклад:
interface Person {
name: string;
age: number;
city: string;
country: string;
}
type StringProperties = PickByType<Person, string>; // type StringProperties = { name: string; city: string; country: string; }
type NumberProperties = PickByType<Person, number>; // type NumberProperties = { age: number; }
PickByType дозволяє створювати новий тип, що містить лише властивості певного типу з існуючого типу.
3. Виведення вкладених типів
infer можна об'єднувати в ланцюжки та вкладати для вилучення типів з глибоко вкладених структур. Наприклад, розглянемо вилучення типу найглибшого елемента вкладеного масиву.
type DeepArrayElement<T> = T extends (infer U)[] ? DeepArrayElement<U> : T;
Пояснення:
T extends (infer U)[]: Це перевіряє, чи єTмасивом, і виводить тип елемента якU.DeepArrayElement<U>: ЯкщоTє масивом, тип рекурсивно викликаєDeepArrayElementз типом елементаU.T: ЯкщоTне є масивом, тип повертає самT.
Приклад:
type NestedStringArray = string[][][];
type DeepString = DeepArrayElement<NestedStringArray>; // type DeepString = string
type MixedNestedArray = (number | string)[][][][];
type DeepMixed = DeepArrayElement<MixedNestedArray>; // type DeepMixed = string | number
type RegularNumber = DeepArrayElement<number>; // type RegularNumber = number
Цей рекурсивний підхід дозволяє витягти тип елемента на найглибшому рівні вкладеності в масиву.
Реальні застосування
Ключове слово infer знаходить застосування в різних сценаріях, де потрібна динамічна маніпуляція типами. Ось кілька практичних прикладів:
1. Створення типобезпечного емулятора подій
Ви можете використовувати infer для створення типобезпечного емулятора подій, який гарантує, що обробники подій отримують правильний тип даних.
type EventMap = {
'data': { value: string };
'error': { message: string };
};
type EventName<T extends EventMap> = keyof T;
type EventData<T extends EventMap, K extends EventName<T>> = T[K];
type EventHandler<T extends EventMap, K extends EventName<T>> = (data: EventData<T, K>) => void;
class EventEmitter<T extends EventMap> {
private listeners: { [K in EventName<T>]?: EventHandler<T, K>[] } = {};
on<K extends EventName<T>>(event: K, handler: EventHandler<T, K>): void {
if (!this.listeners[event]) {
this.listeners[event] = [];
}
this.listeners[event]!.push(handler);
}
emit<K extends EventName<T>>(event: K, data: EventData<T, K>): void {
this.listeners[event]?.forEach(handler => handler(data));
}
}
const emitter = new EventEmitter<EventMap>();
emitter.on('data', (data) => {
console.log(`Received data: ${data.value}`);
});
emitter.on('error', (error) => {
console.error(`An error occurred: ${error.message}`);
});
emitter.emit('data', { value: 'Hello, world!' });
emitter.emit('error', { message: 'Something went wrong.' });
У цьому прикладі EventData використовує умовні типи та infer для вилучення типу даних, пов'язаних з певною назвою події, гарантуючи, що обробники подій отримують правильний тип даних.
2. Реалізація типобезпечного редуктора
Ви можете використовувати infer для створення типобезпечної функції редуктора для управління станом.
type Action<T extends string, P = undefined> = P extends undefined
? { type: T }
: { type: T; payload: P };
type Reducer<S, A extends Action<string>> = (state: S, action: A) => S;
// Example Actions
type IncrementAction = Action<'INCREMENT'>;
type DecrementAction = Action<'DECREMENT'>;
type SetValueAction = Action<'SET_VALUE', number>;
// Example State
interface CounterState {
value: number;
}
// Example Reducer
const counterReducer: Reducer<CounterState, IncrementAction | DecrementAction | SetValueAction> = (
state: CounterState,
action: IncrementAction | DecrementAction | SetValueAction
): CounterState => {
switch (action.type) {
case 'INCREMENT':
return { ...state, value: state.value + 1 };
case 'DECREMENT':
return { ...state, value: state.value - 1 };
case 'SET_VALUE':
return { ...state, value: action.payload };
default:
return state;
}
};
// Usage
const initialState: CounterState = { value: 0 };
const newState1 = counterReducer(initialState, { type: 'INCREMENT' }); // newState1.value is 1
const newState2 = counterReducer(newState1, { type: 'SET_VALUE', payload: 10 }); // newState2.value is 10
Хоча цей приклад безпосередньо не використовує `infer`, він закладає основу для складніших сценаріїв редуктора. `infer` можна застосовувати для динамічного вилучення типу `payload` з різних типів `Action`, дозволяючи суворішу перевірку типів у функції редуктора. Це особливо корисно у великих програмах з численними діями та складними структурами стану.
3. Динамічна генерація типів з відповідей API
При роботі з API ви можете використовувати infer для автоматичного генерування типів TypeScript зі структури відповідей API. Це допомагає забезпечити типобезпеку при взаємодії із зовнішніми джерелами даних.
Розглянемо спрощений сценарій, де ви хочете витягти тип даних із загальної відповіді API:
type ApiResponse<T> = {
status: number;
data: T;
message?: string;
};
type ExtractDataType<T> = T extends ApiResponse<infer U> ? U : never;
// Example API Response
type User = {
id: number;
name: string;
email: string;
};
type UserApiResponse = ApiResponse<User>;
type ExtractedUser = ExtractDataType<UserApiResponse>; // type ExtractedUser = User
ExtractDataType використовує infer для вилучення типу U з ApiResponse<U>, надаючи типобезпечний спосіб доступу до структури даних, що повертається API.
Найкращі практики та міркування
- Ясність та читабельність: Використовуйте описові імена змінних типу (наприклад,
ReturnTypeзамість простоR) для покращення читабельності коду. - Продуктивність: Хоча
inferє потужним, надмірне використання може вплинути на продуктивність перевірки типів. Використовуйте його розсудливо, особливо у великих кодових базах. - Обробка помилок: Завжди надавайте резервний тип (наприклад,
anyабоnever) у гілціfalseумовного типу для обробки випадків, коли тип не відповідає очікуваному шаблону. - Складність: Уникайте надмірно складних умовних типів з вкладеними операторами
infer, оскільки вони можуть стати важкими для розуміння та підтримки. За необхідності рефакторизуйте свій код на менші, більш керовані типи. - Тестування: Ретельно тестуйте свої умовні типи з різними вхідними типами, щоб переконатися, що вони поводяться так, як очікується.
Глобальні міркування
Використовуючи TypeScript та infer у глобальному контексті, врахуйте наступне:
- Локалізація та інтернаціоналізація (i18n): Типи можуть потребувати адаптації до різних локалей та форматів даних. Використовуйте умовні типи та `infer` для динамічної обробки різних структур даних на основі вимог, специфічних для локалі. Наприклад, дати та валюти можуть бути представлені по-різному в різних країнах.
- Проектування API для глобальної аудиторії: Розробляйте свої API з урахуванням глобальної доступності. Використовуйте послідовні структури та формати даних, які легко зрозуміти та обробити незалежно від місцезнаходження користувача. Визначення типів повинні відображати цю послідовність.
- Часові пояси: При роботі з датами та часом враховуйте різницю в часових поясах. Використовуйте відповідні бібліотеки (наприклад, Luxon, date-fns) для обробки перетворень часових поясів та забезпечення точного представлення даних у різних регіонах. Розгляньте представлення дат та часу у форматі UTC у відповідях вашого API.
- Культурні відмінності: Пам'ятайте про культурні відмінності у представленні та інтерпретації даних. Наприклад, імена, адреси та номери телефонів можуть мати різні формати в різних країнах. Переконайтеся, що ваші визначення типів можуть враховувати ці варіації.
- Обробка валют: При роботі з грошовими значеннями використовуйте послідовне представлення валют (наприклад, коди валют ISO 4217) та відповідним чином обробляйте конвертацію валют. Використовуйте бібліотеки, призначені для маніпуляцій з валютами, щоб уникнути проблем з точністю та забезпечити точні розрахунки.
Наприклад, розглянемо сценарій, де ви отримуєте профілі користувачів з різних регіонів, і формат адреси відрізняється залежно від країни. Ви можете використовувати умовні типи та `infer` для динамічного коригування визначення типу на основі місцезнаходження користувача:
type AddressFormat<CountryCode extends string> = CountryCode extends 'US'
? { street: string; city: string; state: string; zipCode: string; }
: CountryCode extends 'CA'
? { street: string; city: string; province: string; postalCode: string; }
: { addressLines: string[]; city: string; country: string; };
type UserProfile<CountryCode extends string> = {
id: number;
name: string;
email: string;
address: AddressFormat<CountryCode>;
countryCode: CountryCode; // Add country code to profile
};
// Example Usage
type USUserProfile = UserProfile<'US'>; // Has US address format
type CAUserProfile = UserProfile<'CA'>; // Has Canadian address format
type GenericUserProfile = UserProfile<'DE'>; // Has Generic (international) address format
Включивши `countryCode` до типу `UserProfile` та використовуючи умовні типи на основі цього коду, ви можете динамічно коригувати тип `address` відповідно до очікуваного формату для кожного регіону. Це дозволяє типобезпечно обробляти різноманітні формати даних у різних країнах.
Висновок
Ключове слово infer є потужним доповненням до системи типів TypeScript, що дозволяє складні маніпуляції та вилучення типів в рамках умовних типів. Оволодівши infer, ви можете створювати більш надійний, типобезпечний та легкопідтримуваний код. Від виведення типів повернення функцій до вилучення властивостей зі складних об'єктів, можливості величезні. Пам'ятайте використовувати infer розсудливо, надаючи пріоритет ясності та читабельності, щоб ваш код залишався зрозумілим та легким для підтримки в довгостроковій перспективі.
Цей посібник надав вичерпний огляд infer та його застосувань. Експериментуйте з наведеними прикладами, досліджуйте додаткові випадки використання та використовуйте infer для покращення вашого робочого процесу розробки на TypeScript.